From c0fc102f97dfdcc94cd9c97e8817902cfcbd5f55 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 23 May 2026 00:22:13 -0700 Subject: [PATCH 1/3] ci: sign OpenUPM package via Unity UPM CLI on release Adds a `sign-and-publish-upm` job to .github/workflows/release.yml that runs Unity's `upm pack` with service-account credentials on every successful version-bump release. The signed .tgz (containing the required `package/.attestation.p7m` attestation) is attached to the GitHub Release alongside the existing Unity package and server zip assets so OpenUPM can serve a signed tarball once its listing is flipped to `trackingMode: githubRelease`. The job is `continue-on-error: true` and short-circuits with a warning when the three required repo secrets (`UPM_SERVICE_ACCOUNT_KEY_ID`, `UPM_SERVICE_ACCOUNT_KEY_SECRET`, `UPM_ORG_ID`) are absent, so the existing release pipeline keeps shipping unchanged until the maintainer configures a Unity-organization service account. docs/openupm-signing.md captures the one-time setup (Unity org + service account, GitHub secrets, and the cross-repo PR against openupm/openupm to flip `trackingMode`), the verification procedure, and troubleshooting notes. Linked from docs/_sidebar.md under "Development". Closes #414 --- .github/workflows/release.yml | 95 ++++++++++++++++++++++++ docs/_sidebar.md | 1 + docs/openupm-signing.md | 136 ++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 docs/openupm-signing.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b25313f9..bff0c03dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -369,6 +369,101 @@ 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: Read package metadata + run: | + package_name="$(jq -r '.name' "$PACKAGE_DIR/package.json")" + package_version="$(jq -r '.version' "$PACKAGE_DIR/package.json")" + + echo "PACKAGE_NAME=$package_name" >> "$GITHUB_ENV" + echo "PACKAGE_VERSION=$package_version" >> "$GITHUB_ENV" + + 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\n' "${#archives[@]}" >&2 + exit 1 + fi + + archive="${archives[0]}" + tar -tzf "$archive" | grep -qx 'package/package.json' + tar -tzf "$archive" | grep -qx 'package/.attestation.p7m' + + echo "PACKAGE_ARCHIVE=$archive" >> "$GITHUB_ENV" + printf 'Signed archive: %s\n' "$(basename "$archive")" + tar -xOzf "$archive" package/package.json | jq '{name, version}' + + - name: Attach signed package to release + uses: softprops/action-gh-release@v2 + with: + files: | + /tmp/signed-upm-dist/*.tgz + /tmp/signed-upm-dist/*.tar.gz + 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..abec2c7df --- /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. + +`githubReleaseAssetName` is **not** required — the release has only one +`.tgz` / `.tar.gz` asset (the signed UPM tarball), so OpenUPM will auto-select +it. If the release ever ships multiple `.tgz` assets, add: + +```yaml +githubReleaseAssetName: 'com.ivanmurzak.unity.mcp-' +``` + +so OpenUPM picks the right one by filename prefix. + +## 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. From c39258c9164543666e545bf9395557408bf42ddc Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 23 May 2026 00:40:30 -0700 Subject: [PATCH 2/3] refactor: simplify drop unused env-var exports in sign-and-publish-upm job; recommend OpenUPM asset prefix unconditionally Apply 03a review-pass findings against the new sign-and-publish-upm job and the OpenUPM signing docs. release.yml: - Drop dead PACKAGE_NAME / PACKAGE_VERSION exports in the metadata step; keep printf as log-only. - Wire the existing PACKAGE_ARCHIVE env export through to the attach step so the exact verified archive is uploaded (was globbing /tmp/signed-upm-dist/*.tgz). - Include archive paths in the 'wrong count' error so a future failure is debuggable from the log alone. docs/openupm-signing.md: - Recommend githubReleaseAssetName unconditionally for the openupm/openupm listing PR; the release already ships multiple asset types and adding a prefix guard is a one-line future-proof. simplify-pass: 1 --- .github/workflows/release.yml | 11 +++-------- docs/openupm-signing.md | 10 +++++----- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bff0c03dd..e8a340157 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -412,14 +412,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v6 - - name: Read package metadata + - name: Log package metadata run: | package_name="$(jq -r '.name' "$PACKAGE_DIR/package.json")" package_version="$(jq -r '.version' "$PACKAGE_DIR/package.json")" - echo "PACKAGE_NAME=$package_name" >> "$GITHUB_ENV" - echo "PACKAGE_VERSION=$package_version" >> "$GITHUB_ENV" - printf 'Package name: %s\n' "$package_name" printf 'Package version: %s\n' "$package_version" @@ -442,7 +439,7 @@ jobs: 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\n' "${#archives[@]}" >&2 + printf 'Expected exactly one signed package archive, found %s: %s\n' "${#archives[@]}" "${archives[*]:-}" >&2 exit 1 fi @@ -457,9 +454,7 @@ jobs: - name: Attach signed package to release uses: softprops/action-gh-release@v2 with: - files: | - /tmp/signed-upm-dist/*.tgz - /tmp/signed-upm-dist/*.tar.gz + files: ${{ env.PACKAGE_ARCHIVE }} tag_name: ${{ needs.release-unity-plugin.outputs.version }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/openupm-signing.md b/docs/openupm-signing.md index abec2c7df..907a3791b 100644 --- a/docs/openupm-signing.md +++ b/docs/openupm-signing.md @@ -93,16 +93,16 @@ 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. -`githubReleaseAssetName` is **not** required — the release has only one -`.tgz` / `.tar.gz` asset (the signed UPM tarball), so OpenUPM will auto-select -it. If the release ever ships multiple `.tgz` assets, add: +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-' ``` -so OpenUPM picks the right one by filename prefix. - ## Verifying signing worked After the next release that runs with the secrets in place: From 3f4dd06f0dde7cd130f02fa36b8b94157a6f582d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Sat, 23 May 2026 00:50:26 -0700 Subject: [PATCH 3/3] refactor: simplify enforce OpenUPM asset prefix in release verify step Address pass-2 review findings on the sign-and-publish-upm job's verify step: - Assert the signed archive basename begins with the documented `com.ivanmurzak.unity.mcp-` prefix that docs/openupm-signing.md tells OpenUPM to consume via `githubReleaseAssetName`. Without this guard, a future `upm pack` naming change would let CI pass while OpenUPM silently failed to pick up the signed tarball. - Capture `tar -tzf` output into a single variable, then grep it twice locally. Saves one tarball walk per release; cosmetic but removes a duplicated traversal. simplify-pass: 2 --- .github/workflows/release.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e8a340157..bf7c7a1e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -444,11 +444,21 @@ jobs: fi archive="${archives[0]}" - tar -tzf "$archive" | grep -qx 'package/package.json' - tar -tzf "$archive" | grep -qx 'package/.attestation.p7m' + 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' "$(basename "$archive")" + printf 'Signed archive: %s\n' "$archive_basename" tar -xOzf "$archive" package/package.json | jq '{name, version}' - name: Attach signed package to release